﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Cmd.Internal;
using System.Collections;
using Cmd.TypeConverters;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;

namespace Cmd
{
	public class CommandLineParser<T> where T : OptionSet
	{
		private static PropertyInfo[] _props;

		private CommandLineParser()
		{
		}

		public static T GetOptions()
		{
			return GetOptions(null);
		}

		public static T GetOptions(string cmd)
		{
			bool autoExit = false;
			if (string.IsNullOrWhiteSpace(cmd))
			{
				cmd = Environment.CommandLine;
				autoExit = true;
			}
			T commandLineOptions = (T)Activator.CreateInstance(typeof(T));
			DisplayHeader(commandLineOptions);
			_props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);

			bool success = true;
			try
			{
				CommandLineParser<T> parser = new CommandLineParser<T>();
				success = SetOptionSet(cmd, commandLineOptions);
			}
			catch (Exception ex)
			{
				(commandLineOptions as OptionSet)._errors.Add(ex.Message);
			}

			if (!success)
			{
				commandLineOptions.ShowHelp(GetAllOptionAttributes());
				if (autoExit)
				{
					Environment.Exit(commandLineOptions._errors.Count == 0 ? 0 : 1);
				}
			}
			else
			{
				string[] customErrorMessages = null;
				commandLineOptions._isValid = commandLineOptions.Validate(out customErrorMessages) && commandLineOptions._errors.Count == 0;
				if (customErrorMessages != null && customErrorMessages.Length > 0)
				{
					commandLineOptions._errors.AddRange(customErrorMessages);
				}
				if (commandLineOptions._innerList.Exists(item => item.IsHelp) || !commandLineOptions._isValid)
				{
					commandLineOptions.ShowHelp(GetAllOptionAttributes());
					if (autoExit)
					{
						Environment.Exit(commandLineOptions._errors.Count == 0 ? 0 : 1);
					}
				}
			}
			return commandLineOptions;
		}

		private static void DisplayHeader(OptionSet options)
		{
			foreach (string info in options.GetAssemblyInfo())
			{
				Console.WriteLine(info);
			}
			Console.WriteLine();
		}

		/// <summary>
		/// Parses the actual option arguments
		/// </summary>
		/// <param name="command"></param>
		/// <param name="commandLineOptions"></param>
		/// <returns>true if succedes or false to instantly show help.</returns>
		private static bool SetOptionSet(string command, T commandLineOptions)
		{
			string args = RawParser.ParseRawArguments(command);
			HashSet<KeyValuePair<string, string>> rawValueList = RawParser.ParseRawValues(args);
			bool isHelp = rawValueList.Count(item => item.Key == "?" || item.Key == "help") == 0;
			if (!isHelp)
			{
				return false;
			}
			TypeConverterHelpers.RefreshConverters(commandLineOptions as OptionSet);


			int idx = 0;
			while (idx < rawValueList.Count)
			{
				CommandLineOption option = GetOption(rawValueList.ElementAt(idx), commandLineOptions);
				if (option != null)
				{
					(commandLineOptions as OptionSet)._innerList.Add(option);
					rawValueList.Remove(rawValueList.ElementAt(idx));
				}
				else
				{
					idx++;
				}
			}

			List<CommandLineOption> defaultOptions = GetMissingOptionsWithDefaultValue(commandLineOptions as OptionSet);
			if (defaultOptions != null)
			{
				(commandLineOptions as OptionSet)._innerList.AddRange(defaultOptions);
			}

			if (rawValueList.Count > 0)
			{
				//check for remaining options as boolean flags
				idx = 0;
				int count = 0;
				while (idx < rawValueList.Count)
				{
					count = 0;
					char[] opts = rawValueList.ElementAt(idx).Key.ToCharArray();
					foreach (char c in opts)
					{
						KeyValuePair<string, string> kv = new KeyValuePair<string, string>(c.ToString(), null);
						CommandLineOption option = GetOption(kv, commandLineOptions);
						if (option != null)
						{
							(commandLineOptions as OptionSet)._innerList.Add(option);
							count++;
						}
					}
					if (count == opts.Length)
					{
						rawValueList.Remove(rawValueList.ElementAt(idx));
					}
					else
					{
						commandLineOptions._errors.Add(string.Format("Invalid option: {0}", rawValueList.ElementAt(idx).Key));
						idx++;
						continue;
					}
				}
			}

			commandLineOptions._groups = GetAllOptionGroups();

			//determine the active group
			if (commandLineOptions._errors.Count == 0)
			{
				List<OptionGroup> activeGroups = commandLineOptions._groups.Where(grp => commandLineOptions._innerList.Exists(iCmd => iCmd.Groups.Contains(grp.Name)) || grp.Name == "__Default__").ToList();
				bool isMultipleActiveGroups = activeGroups != null && activeGroups.Count(grp => grp.Name != "__Default__") != 1;
				bool isDefaultGroupOnly = activeGroups != null && activeGroups.Count == 1 && activeGroups.Exists(grp => grp.Name == "__Default__");
				if (activeGroups == null ||
					(isMultipleActiveGroups && !isDefaultGroupOnly))
				{
					commandLineOptions._errors.Add("Invalid parameters!");
				}
				else if (!isDefaultGroupOnly)
				{
					OptionGroup actGrp = activeGroups.FirstOrDefault(grp => grp.Name != "__Default__");
					if (actGrp == null)
					{
						bool hasRequiredOptions = commandLineOptions._groups.Exists(grp => grp.Options.FirstOrDefault(opt => opt.Required) != null);
						if (hasRequiredOptions)
						{
							commandLineOptions._errors.Add("Invalid parameters!");
						}
					}
					else
					{
						commandLineOptions._activeGroup = activeGroups[0].Name == "__Default__" ? null : activeGroups[0].Name;
					}
				}
			}
			if (commandLineOptions._errors.Count == 0)
			{
				//check required and not defined options
				foreach (PropertyInfo info in _props)
				{
					if (!info.IsDefined(typeof(OptionAttribute), true))
					{
						continue;
					}
					OptionAttribute opt = GetOptionAttribute(info);
					if (opt == null)
					{
						continue;
					}

					if (opt.Required)
					{
						if (!commandLineOptions._innerList.Exists(item => item.Command == opt.Command))
						{
							commandLineOptions._errors.Add(string.Format("Option '-{0}' is required and not defined!", opt.Command));
						}
					}
					else if (commandLineOptions._innerList.Exists(item => item.Command == opt.Command && item.Groups.Contains(commandLineOptions._activeGroup)))
					{
						bool isrequiredInGroup = commandLineOptions._groups.Exists(grp => grp._options.Exists(optItem => opt.Command == optItem.Option.Command && optItem.Required));
						if (isrequiredInGroup && !commandLineOptions._innerList.Exists(item => item.Command == opt.Command))
						{
							commandLineOptions._errors.Add(string.Format("Option '-{0}' is required and not defined!", opt.Command));
						}
					}
				}
			}

			return true;
		}

		private static CommandLineOption GetOption(KeyValuePair<string, string> rawValue, OptionSet commandLineOptions)
		{
			CommandLineOption option = null;// GetSystsemOption(rawValue);
			//if (option != null)
			//{
			//    return option;
			//}
			foreach (PropertyInfo info in _props)
			{
				if (!info.IsDefined(typeof(OptionAttribute), true))
				{
					continue;
				}
				OptionAttribute opt = GetOptionAttribute(info);
				if (opt == null)
				{
					continue;
				}
				if (string.IsNullOrWhiteSpace(rawValue.Key) || (opt.Command != rawValue.Key && opt.Alias != rawValue.Key))
				{
					continue;
				}
				object propValue = info.GetValue(commandLineOptions, null);

				option = new CommandLineOption(opt.Command);
				object val = null;
				if (string.IsNullOrEmpty(rawValue.Value))
				{
					//option found, defined, but null
					val = opt.DefaultValue;
					if (val == null && info.PropertyType == typeof(bool))
					{
						//true as default value for boolean flag
						val = true;
					}
				}
				else
				{
					try
					{
						//attribute found and defined
						val = rawValue.Value.FromString(info.PropertyType);
					}
					catch (Exception ex)
					{
						commandLineOptions._errors.Add(string.Format("Cannot parse parameter '-{0}': {1}", opt.Command, ex.Message));
					}
				}

				info.SetValue(commandLineOptions, val, null);
				option.Value = val;

				option.Alias = opt.Alias;
				if (commandLineOptions._innerList.Contains(option, new CommandLineOptionEqComparer()))
				{
					commandLineOptions._errors.Add(string.Format("Option {0} already exists.", option));
					return null;
				}

				option.IsDefined = !string.IsNullOrWhiteSpace(rawValue.Key);
				option.PropertyName = info.Name;
				option.Name = opt.Name;
				option.Description = opt.Description;
				option.Required = opt.Required;
				option.Type = info.PropertyType;
				option.Groups.AddRange(GetOptionGroupAttributes(info).Select(grpAtt => grpAtt.Name));
			}
			return option;
		}

		//private static CommandLineOption GetSystsemOption(KeyValuePair<string, string> rawValue)
		//{
		//    switch (rawValue.Key)
		//    {
		//        case "?":
		//        case "help":
		//            return new CommandLineOption(rawValue.Key) { IsHelp = true, Description = "Shows this help screen." };
		//        default:
		//            break;
		//    }
		//    return null;
		//}

		private static List<CommandLineOption> GetMissingOptionsWithDefaultValue(OptionSet commandLineOptions)
		{
			List<CommandLineOption> optionList = new List<CommandLineOption>();
			foreach (PropertyInfo info in _props)
			{
				if (!info.IsDefined(typeof(OptionAttribute), true))
				{
					continue;
				}
				OptionAttribute opt = GetOptionAttribute(info);
				if (opt == null || opt.DefaultValue == null || commandLineOptions._innerList.Exists(item => item.Command == opt.Command))
				{
					continue;
				}

				object propValue = info.GetValue(commandLineOptions, null);

				CommandLineOption option = new CommandLineOption(opt.Name);
				object val = opt.DefaultValue;
				info.SetValue(commandLineOptions, val, null);

				option.Alias = opt.Alias;
				option.Command = opt.Command;
				if (commandLineOptions._innerList.Contains(option, new CommandLineOptionEqComparer()))
				{
					commandLineOptions._errors.Add(string.Format("Option {0} already exists.", option));
					return null;
				}

				option.Value = val;
				option.IsDefined = false;
				option.PropertyName = info.Name;
				option.Name = opt.Name;
				option.Description = opt.Description;
				option.Required = opt.Required;
				option.Type = info.PropertyType;
				option.Groups.AddRange(GetOptionGroupAttributes(info).Select(grpAtt => grpAtt.Name));

				optionList.Add(option);
			}
			return optionList;
		}

		private static UniqueCollection<OptionGroup> GetAllOptionGroups()
		{
			UniqueCollection<OptionGroup> lst = new UniqueCollection<OptionGroup>();

			foreach (PropertyInfo info in _props)
			{
				if (!info.IsDefined(typeof(OptionAttribute), true))
				{
					continue;
				}
				OptionAttribute opt = GetOptionAttribute(info);
				if (opt == null)
				{
					continue;
				}
				List<OptionGroupAttribute> optGroupList = GetOptionGroupAttributes(info);
				if (optGroupList.Count == 0)
				{
					OptionGroup grp = lst.Where(item => item.Name == "__Default__").FirstOrDefault();
					if (grp == null)
					{
						grp = new OptionGroup("__Default__");
						lst.Add(grp);
					}
					grp._options.Add(new OptionGroupItem(opt));
				}
				else
				{
					foreach (OptionGroupAttribute att in optGroupList)
					{
						OptionGroup grp = lst.Where(item => item.Name == att.Name).FirstOrDefault();
						if (grp == null)
						{
							grp = new OptionGroup(att.Name);
							lst.Add(grp);
						}
						//if required field is null, it is inherited from the option attribute
						grp._options.Add(new OptionGroupItem(opt, att._required));
					}
				}
			}

			return lst;
		}

		private static List<OptionGroupAttribute> GetOptionGroupAttributes(PropertyInfo property)
		{
			List<OptionGroupAttribute> optGroups = new List<OptionGroupAttribute>();
			if (!property.IsDefined(typeof(OptionAttribute), true))
			{
				return optGroups;
			}
			object[] attributeList = property.GetCustomAttributes(true);
			if (attributeList == null)
			{
				return optGroups;
			}
			foreach (object attribute in attributeList)
			{
				if (attribute.GetType() == typeof(OptionGroupAttribute))
				{
					optGroups.Add(attribute as OptionGroupAttribute);
				}
			}

			return optGroups;
		}

		private static OptionAttribute[] GetAllOptionAttributes()
		{
			List<OptionAttribute> optList = new List<OptionAttribute>();
			foreach (PropertyInfo info in _props)
			{
				if (!info.IsDefined(typeof(OptionAttribute), true))
				{
					continue;
				}
				OptionAttribute opt = GetOptionAttribute(info);
				if (opt == null || optList.Exists(item => item.Command == opt.Command))
				{
					continue;
				}

				optList.Add(opt);
			}
			return optList.ToArray();
		}

		private static OptionAttribute GetOptionAttribute(PropertyInfo property)
		{
			object[] attributeList = property.GetCustomAttributes(true);
			if (attributeList == null)
			{
				return null;
			}
			foreach (object attribute in attributeList)
			{
				if (attribute.GetType() == typeof(OptionAttribute))
				{
					return attribute as OptionAttribute;
				}
			}
			return null;
		}
	}
}
